
module(..., package.seeall)

require("cosmo")


--- Creates the HTML for the navigation bar.
--
--  @param page          A page table.
--  @param params        CGI parameters (not used).
--  @param sputnik       The Sputnik object.
--  @return              An HTML string.

function get_nav_bar (page, params, sputnik)

   cur_page = sputnik:dirify(page.name)          

   local nav_page, error = sputnik:load_config_page(
                 sputnik.config.DEFAULT_NAVIGATION_BAR)
   
   if error or (not nav_page.NAVIGATION) then
      return "Couldn't parse the navigation bar: " .. (error or "")
   end

   local nav = nav_page.NAVIGATION

   for i, section in ipairs(nav_page.NAVIGATION) do
      if not section.title then section.title = section.id end
      section.id = sputnik:dirify(section.id)
      if section.id == cur_page or section.id == page.category then
         section.is_active = true
         nav.current_section = section
      end

      for j, subsection in ipairs(section) do
         if not subsection.title then subsection.title = subsection.id end
         subsection.id = sputnik:dirify(subsection.id)
         if subsection.id == cur_page or subsection.id == page.category then
            section.is_active = true
            nav.current_section = section
            subsection.is_active = true
         end
      end
   end

   return cosmo.fill(page.templates.NAV_BAR,
      {  do_sections = function() 
            for i, section in ipairs(nav) do
               local class = "back"
               if section.is_active then 
                  class = "front"
               end
               cosmo.yield { 
                  class = class,
                  id    = section.id,
                  link   = sputnik:make_link(section.id),  
                  label = section.title}
            end
         end,
         do_subsections = function() 
            for i, subsection in ipairs(nav.current_section or {}) do
               local class = "back"
               if subsection.is_active then
                  class = "front"
               end
               cosmo.yield { 
                  class = class,
                  link = sputnik:make_link(subsection.id),
                  label = subsection.title 
               }
            end
         end,
      }
   )
end


--- Creates the HTML for the navigation bar.
--
--  @param page          A page table.
--  @param sputnik       The Sputnik object.
--  @return              An HTML string.

function get_history_table (page, sputnik)
   if not page.history then return {} end
   return {  
      version          = page.version,
      author           = page.author,
      if_only_version  = cosmo.cond(#page.history.all < 2, {}),
      if_last_version  = cosmo.cond((page.version == page.history.current) 
                                    and page.history.previous,
                                    { diff_link = page.links:diff{
                                          version = page.history.current,
                                          other = page.history.previous
                                       }
                                    }
                                 ),
      if_exists_later  = cosmo.cond(not(page.version == page.history.current),
                                    { diff_link   = page.links:diff{
                                          version = page.history.current,
                                          other = page.version
                                       }
                                    }
                                 ),
      if_exist_earlier = cosmo.cond(page.history.previous,
                                    { older_link = page.links:show{
                                          version = page.history.previous
                                       }
                                    }
                                 ),
      history_link      = page.links:history(),
   }
end


local HtmlDiff = {}  -- a class do diff

--- Splits strings into tokens.
--
--  @param text          A string to be split.
--  @return              A list of tokens.

function HtmlDiff.diffsplit(self, text) 
   local l = {}
   text = text .. " "
   for token, sep in string.gmatch(text, "([^%s]*)(%s)") do
      l[#l+1] = token
      l[#l+1] = sep
   end
   return l
end

--- Returns a diff between two strings as HTML.
--
--  @param t1            One string.
--  @param t2            Another string.
--  @return              An HTML string.

function HtmlDiff.get_html_diff(self, t1, t2)

   local html = "" --<pre><code>"
   local t2 = self:diffsplit(t2)
   local t1 = self:diffsplit(t1)
      
   local l = {}
   require("lcs")
   
   local subseq = lcs.longestCommonSubseq(t1, t2, l)
   
   is = 1
   i1 = 0
   i2 = 1
   
   local state = ""
   
   function indiff(text)
      if #text > 0 then 
         return "<ins class='diffmod'>" .. text .. "</ins>"
      else
         return ""
      end
   end
   
   function outdiff(text) 
      if #text > 0 then
         return "<del class='diffmod'>" .. text .. "</del>"
      else 
         return ""
      end
   end

   while true do
      
      i1 = i1 + 1
      
      if i1 > #t1 then                                  -- done with t1 
         for j=i2,#t2 do
            html = html .. indiff(t2[j])
         end
         for j=is,#subseq do
            html = html .. subseq[j]
         end
         break                                          -- done
      end

      if t1[i1] == subseq[is] then 
         while true do   ---- fastforward through T2 ---
            if t2[i2] == t1[i1] or i2 > #t2 then 
               break 
            else
               if t2[i2] then 
                  html = html .. indiff(t2[i2]) 
               end
               i2 = i2 + 1
            end
         end
         
         --html = html .. outdiff(t1[i1])
         i2 = i2 + 1
         if subseq[is] then 
            html = html .. subseq[is]
         end
         is = is + 1
      else 
         html = html .. outdiff(t1[i1])
      end
   end
   
   html = html .. "</code></pre>"
   return html
end


-- ---------------------------------------------------------------------
--     Actions
-- ---------------------------------------------------------------------

actions = {}

--- Saves the new page submitted as a single string param, then
--- returns the HTML representation of the page.
--
--  @param params        params.raw_page is should contain the raw
--                       representation of the page.
--  @param sputnik       The sputnik table is used to save and reload 
--                       the page.

function actions.raw_save(page, params, sputnik)
   if not params.user then
      params.try_again = NOT_LOGGED_IN
      return actions.raw_edit(page, params, sputnik)
   else
      sputnik:save_page(page, params.raw_page)
      page = sputnik:get_page(page.name)
      page = sputnik:decorate_page(page)
      return page.actions.show(page, params, sputnik)
   end
end



--- Saves the page submitted as a set of cgi params, then returns the
--- HTML representation of the page.
--
--  @param params        params fields are used to update the page.
--  @param sputnik       The sputnik table is used to save and reload 
--                       the page.  

function actions.save(page, params, sputnik)
   if not params.user then
      params.try_again = NOT_LOGGED_IN
      return actions.edit(page, params, sputnik)
   else
      local tmp_page = sputnik:update_page_with_params(page, params)
      new_page = sputnik:decorate_page(tmp_page)
      sputnik:save_page(new_page)
      return new_page.actions.show(new_page, params, sputnik)
   end
end



--- Returns just the content of the page, without the navigation bar,
--- the tool bars, etc.
--  
--  @param params        (Not used.)
--  @param sputnik       Used to wikify the page.

function actions.show_content(page, params, sputnik)
   return sputnik:wikify_text(page.content, page.templates)
end



--- Returns the complete HTML for the page.
--
--  @param params        (Not used.)
--  @param sputnik       

function actions.show(page, params, sputnik)
   page.inner_html = page.actions.show_content(page, params, sputnik)
   return page.wrappers.default(page, params, sputnik)
end



--- Same as "show_content" but updates the page with values in
--- parameters before displaying it (used by both AHAH preview and
--- server-side preview).
--
--  @param params        params fields are used to update the page.
--  @param sputnik       The sputnik table is used to reload the page.

function actions.preview_content(page, params, sputnik)
   local tmp_page = sputnik:update_page_with_params(page, params)
   new_page = sputnik:decorate_page(tmp_page)
   return new_page.actions.show_content(new_page, params, sputnik)
end



--- Returns HTML showing a preview of the page (based on cgi params)
--- and also a form to continue editing the page (the page is _not_
--- saved).
--
--  @param params        params fields are used to update the page.
--  @param sputnik       The sputnik table is used to save and reload 
--                       the page.  
--

function actions.preview(page, params, sputnik)
   params.preview = actions.preview_content(page, params, sputnik)
   return actions.edit(page, params, sputnik)
end


--- An action used for HTML post to make it possible to translate
--- button names.  It is highly useful to use button selection to
--- choose the action i.e. choose between save, preview, and cancel,
--- without requiring one uber-action that has special code to check
--- the value of the submit button.  But we can't just set the button
--- name to "action" because button labels==values and different
--- language templates will want to rewrite the value so the button
--- text will be meaningful to that language solution: use a shim
--- action that looks at what action_* named param we have and call
--- that (true) action
--
--  @param params        We'll look for params.action_* to figure out 
--                       what we should be actually doing.
--  @return              HTML (whatever is returned by the action that
--                       it dispatches to).

function actions.postbutton(page, params, sputnik)
   for k,v in pairs(params) do
      local action = string.match(k, "^action_(.*)$")
      if action then
         -- call the real target
         return actions[action](page, params, sputnik) 
      end
   end
end



--- Logs out the user and returns an HTML string to let them know.
--
--

function actions.logout(page, params, sputnik)
   params.user=nil
   page.inner_html = cosmo.fill(page.templates.LOGGED_OUT,                   
                                {  link   = page.links:show(),
                                   title = page.name } )
   return page.wrappers.default(page, params, sputnik)
end



--- Returns history of changes for _all_ pages, by setting a
--- params.show_complete_history and then forwarding to
--- actions.history.

function actions.complete_history(page, params, sputnik)
   params.show_complete_history = 1
   return page.actions.history(page, params, sputnik)  
end


local MONTHS = { 
   {"Jan", 31}, {"Feb", 28}, {"Mar", 31}, {"Apr", 30}, 
   {"May", 31}, {"Jun", 30}, {"Jul", 31}, {"Aug", 31}, 
   {"Sep", 30}, {"Oct", 31}, {"Nov", 30}, {"Dec", 31} 
}

--- Return HTML representing history of one page or all pages.
--
--  @param params        params.show_complete_history will determine 
--                       whether we show history for just this page 
--                       or for all pages.  params.date is an optional 
--                       filter

function actions.history(page, params, sputnik)

   local page_name = page.name
   local datelink = function(month, date, template) 
      local params = { date = string.format(template, month, date) }
      return page.links:history(params)
   end

   if params.show_complete_history then
      page_name = nil
      datelink = function(month, date, template) 
         return sputnik:pseudo_page("Special:History").links:show{
            date = string.format(template, month, date) 
         }
      end
   end
   local title_filter = function(x) return x end

   local limit = 50
   if params.date and params.date:len() == 10 then 
      limit = 200
   end

   local pages = sputnik:get_history(page_name, limit, params.date)

   local date = params.date 
   if not date then
      local d = os.date("*t")
      date = string.format("%02d-%02d-%02d", d.year, d.month, d.day)
   end
   local cur_month = date:sub(1,7)
   local cur_mm = date:sub(6,7)
   if cur_mm:len() > 0 then cur_mm = tonumber(cur_mm) end
   local cur_year = date:sub(1,4)
   local cur_date = date:sub(9,11)
   if cur_date:len() > 0 then cur_date = tonumber(cur_date) end

   tab = { 
      do_revisions = function()
         for i, p in ipairs(pages) do
            cosmo.yield{
               version_link = p.links:show{version = p.version},
               version     = p.version,
               title       = title_filter(p.title),
               author_link  = sputnik:make_link("User:" .. (p.author or "???")),
               author      = p.author,
               if_summary  = cosmo.cond(p.summary 
                                        and p.summary:len() > 0,
                                        {summary = p.summary}),
            }
         end
      end, 
      current_month = cur_month,
      current_year = cur_year,
      do_dates = function() 
         for i=1,MONTHS[cur_mm][2] do
            cosmo.yield {
               if_current_date = cosmo.cond( i == cur_date,
                                            {  date = i}
                                         ),
               if_other_date   = cosmo.cond( not (i == cur_date),
                                            {  date = i,
                                               date_link = datelink(cur_month, 
                                                                    i,
                                                                    "%s-%02d")
                                            }
                                         ),
            }
         end
      end,
      do_months = function() 
         for i=1,12 do
            cosmo.yield {
               if_current_month = cosmo.cond( i == 5, 
                                             {  month = MONTHS[i][1],}
                                          ),
               if_other_month   = cosmo.cond( not (i == 5),
                                             {  month = MONTHS[i][1],
                                                month_link = datelink(
                                                   i, nil, "2007-%02d")
                                             }
                                          ),
            }
         end
      end
   }

   page.inner_html = cosmo.fill(page.templates.HISTORY, tab)
   return page.wrappers.default(page, params, sputnik)
end



--- Same as "show_complete_history" but returns RSS instead of HTML.
--
--

function actions.complete_history_rss(page, params, sputnik)
   params.show_complete_history = 1
   return actions.rss(page, params, sputnik)  
end



--- Same as "history" but returns RSS instead of HTML.
--
--

function actions.rss(page, params, sputnik)
   local page_name = page.name
   local title = "Recent Edits to '" .. page.title .."'"
   if params.show_complete_history then
      page_name = nil
      title = "Recent Wiki Edits"
   end
   local history = sputnik:get_history(page_name, 50) 
   return cosmo.fill(page.templates.RSS, {  
      title   = title,
      baseurl = sputnik.config.BASE_URL, 
      items   = function()
                   for i, page in ipairs(history) do
                      cosmo.yield{
                         link        = "http://" .. sputnik.config.DOMAIN ..
                                       sputnik:escape_url(
                                          page.urls:show{version=page.version}
                                       ),
                         title       = string.format("%s: %s by %s",
                                                     page.version,
                                                     page.title or "",
                                                     page.author or ""),
                         ispermalink = "false",
                         guid        = page.name .. "/" .. page.version,
                         summary     = page.summary
                      }
                   end
                end,
   }), "application/rss+xml"
end



--- Displays HTML for a login form.
--
--

function actions.login_form(page, params, sputnik)
   page.inner_html = cosmo.fill(page.templates.LOGIN, 
                                {  action_url = sputnik.config.BASE_URL,
                                   page_name  = sputnik:dirify(page.name) } )
   return page.wrappers.default(page, params, sputnik)
end

NOT_LOGGED_IN = "not_logged_in"



--- Shows HTML for a "raw edit" form (a form that allows the user to
--- edit the underlying string representation of the page).
--
--

function actions.raw_edit (page, params, sputnik)
   params.raw_edit = 1
   if params.try_again then
      page.raw = params.raw_page
   end
   return actions.edit(page, params, sputnik)
end



--- Shows HTML for the standard Edit field.
--
--

function actions.edit (page, params, sputnik)

   -- select the parameters that should be copied
   local fields = {}
   for i, field in ipairs(sputnik.storage:get_saveable_fields()) do
      fields[field] = params[field] or page.as_is[field]
   end
   
   fields.name = page.name  -- page name cannot be changed

   local template
   local bad_fields = {} -- any fields that this forms won't preserve

   if params.raw_edit then 
      template = page.templates.RAW_EDIT
      -- raw_edit option is lossless.
   else
      template = page.templates.EDIT
      -- check if there are any fields that this page won't be able to
      -- preserve
      if page.raw:len() > 0 then
         local reloaded_page = sputnik:load_config(page.raw, {})
         for k,v in pairs(reloaded_page) do
            if reloaded_page[k] and sputnik.storage:is_extra_field(k) then
               table.insert(bad_fields, k)
            end
         end
      end
   end

   page.inner_html = cosmo.lazy_fill(template,
      {
         if_bad_fields    = cosmo.cond(#bad_fields > 0,
                                     {offenders = table.concat(bad_fields, 
                                                               ", ")
                                     }
                                  ),
         if_try_again     = cosmo.cond(params.try_again == NOT_LOGGED_IN, {}),
	 if_preview	  = cosmo.cond(params.preview, 
				       { 
					  preview = params.preview,
					  summary = fields.summary
				       }),
	 if_minor         = cosmo.cond(fields.minor ~= "", {}),
	 if_logged_in     = cosmo.cond(params.user,
				       {  user = params.user,
					  logout_url = page.urls:logout()
				       }),
	 if_not_logged_in = cosmo.cond(not params.user, {}),
	 action_url       = sputnik.config.BASE_URL,
	 page_name        = sputnik:dirify(fields.name),
	 title            = fields.title,
	 category         = fields.category,
	 metapage         = fields.metapage,
	 templates        = fields.templates,
	 actions          = fields.actions,
	 content          = sputnik:escape(fields.content),
	 raw              = sputnik:escape(page.raw),
	 edit_link        = page.urls:edit(),
	 raw_edit_link    = page.urls:raw_edit(),
	 sputnik_site     = "http://www.freewisdom.org/projects/sputnik",

      } )
   return page.wrappers.default(page, params, sputnik)
end

--- Shows HTML of diff between two versions of the page.
--
--

function actions.diff(page, params, sputnik)
   local other_page = sputnik:get_page(page.name, params.other) 
   page.inner_html  = cosmo.fill(page.templates.DIFF,
      {  version1 = params.version,
	 url1     = page.urls:show{version=params.version},
	 author1  = page.author,
         version2 = params.other,
	 url2     = page.urls:show{version=params.other},
	 author2  = other_page.author,
	 diff     = HtmlDiff:get_html_diff(other_page.content, page.content) 
      } )
   return page.wrappers.default(page, params, sputnik)
end

--- Shows the raw content of the page with content-type set to
--- text/plain (note that unlike actions.raw, this method only returns
--- the _content_ of the page, not its metadata).

function actions.raw_content(page, params, sputnik)
   return page.content, "text/plain"
end



--- Shows the underlying string representation of the page as plain text.
--- 
--

function actions.raw(page, params, sputnik)
   return page.raw or "No source available.", "text/plain"
end



--- Shows the _content of the page shown as 'code'.
--
--

function actions.show_content_as_code(page, params, sputnik)
   local escaped = sputnik:escape(page.content) 
   return "<pre><code>" .. escaped .. "</code></pre>"
end



--- Shows the content of the page as Lua code, checking whether it
--  parses.

function actions.show_content_as_lua_code(page, params, sputnik)

   local DOLLAR_REPLACEMENT = "$<span></span>"
   local escaped = sputnik:escape(page.content)
   local escaped = escaped:gsub("%$", DOLLAR_REPLACEMENT)
   local escaped = escaped:gsub(" ", "&nbsp;")
   escaped = string.gsub(escaped, "(%-%-[^\n]*)", 
			 function (comment) 
			    return "<font color='gray'>"
			       ..comment.."</font>"
			 end)
   local f, errors = loadstring(page.content)
   if errors then
      local reg_exp = "^.+%]%:(%d+)%:"
      error_line_num = string.match(errors, reg_exp)
      errors = string.gsub(errors, reg_exp, "On line %1:")
   end

   return cosmo.fill(page.templates.LUA_CODE,
      {  do_lines  = function()
			local i = 0
			for line in ("\n"..escaped):gmatch"\n([^\n]*)" do
			   i = i+1
			   local class = "ok"
			   if i == tonumber(error_line_num) then
			      class = "bad"
			   end
			   cosmo.yield{
			      i = i, 
			      line = line,
			      class=class
			   }
			end
		     end,
	 if_ok     = cosmo.cond (f, {}),
	 if_errors = cosmo.cond(errors, {errors=errors})
      }
   )
end


--- Shows the HTML of the whole page, with the content of the page
--  shown as 'code'.

function actions.code(page, params, sputnik)
   page.inner_html = page.actions.show_content_as_code(page, params, sputnik)
   return page.wrappers.default(page, params, sputnik)
end



--- Shows the HTML for an error message when a non-existent action is
--  requested.

function actions.action_not_found(page, params, sputnik)
   page.inner_html  = cosmo.fill(page.templates.ACTION_NOT_FOUND,
      {  title             = page.title,
	 url               = page.urls:show(),
	 action            = params.action,
	 if_custom_actions = cosmo.cond(page.as_is.actions and
					page.as_is.actions:len() > 0,
					{ actions = page.as_is.actions }
				     )
      } )
   return page.wrappers.default(page, params, sputnik)
end




wrappers = {}



--- Wraps the HTML content in bells and whistles such as the
--- navigation bar, the header, the footer, etc.

function wrappers.default(page, params, sputnik) 
   --local nav_page = sputnik.config.NAVIGATION_OVERRIDES[page.name] 
   --                 or sputnik.config.DEFAULT_NAVIGATION_BAR

   local cookie = ""
   if params.user and params.auth_token then
      cookie = params.user .. "|" .. params.auth_token
   end

   return cosmo.fill( page.templates.MAIN, {  
      site_title       = sputnik.config.SITE_TITLE or "",
      title            = page.title,
      page_rss_link    = page.links:rss(),
      do_stylesheets   = function()
			    for i, url in ipairs(sputnik.config.STYLESHEETS) do
			       cosmo.yield {url=url}
			    end
			 end,
      nav_bar          = get_nav_bar(page, params, sputnik),
      cookie_name      = sputnik.config.COOKIE_NAME,
      cookie_value     = cookie,
      if_old_version   = cosmo.cond(params.version 
				    and not (page.version 
					     == page.history.current),
				    {version = params.version} 
				 ),
      if_logged_in     = cosmo.cond( params.user,
				    {  user        = params.user,
				       logout_link = page.links:logout() }
				 ),
      if_not_logged_in = cosmo.cond( not params.user,
				    { login_form_link 
                                                   = page.links:login_form() }
				 ),
      if_edit_button   = cosmo.cond( not ( params.action == "login_form" 
					   or params.action == "logout") ,
		            {  talk_link    = sputnik:make_link(
					        sputnik:talk(page.name)),
			       page_link    = sputnik:make_link(
					        sputnik:untalk(page.name)),
			       edit_link    = page.links:edit{
				  version = params.version 
			       },
			       history_link = page.links:history()
			    }
			 ),
      content          = page.inner_html,
      sputnik_link     = "href='http://www.freewisdom.org/projects/sputnik/'",
      rss_link         = sputnik:pseudo_page("Special:History").links:rss(),
      if_has_history   = cosmo.cond(page.history and page.author, 
				    get_history_table(page, sputnik)),
      if_no_history    = cosmo.cond(not (page.history and page.author),{}),
      sidebar          = "",

   } )

end
